﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser
{
    #region Using Statements

    using Jint;
    using System;
    using System.IO;
    using System.Net;
    using System.Text;
    using Jint.Native;
    using System.Reflection;
    using KLF.Lib.Browser.JavaScriptHelpers;
    using KLF.Lib.Browser.DOM;
    using KLF.Lib.Browser.DOM.HTML;
    using KLF.Lib.Browser.DOM.Events;
    using System.Collections.Generic;

    #endregion

    public class RequestContext : IDisposable
    {
        #region Constructors

        private RequestContext()
        {
            PostVariables = new Dictionary<string, string>();
            Verb = HTMLConstants.HTTP_VERB_GET;
        }

        public RequestContext(RequestContext parent, BrowserWindow window, Uri uri)
            : this()
        {
            Browser = window.Owner;
            Agent = Browser.Options.Agent;
            Cookies = parent != null ? parent.Cookies : Browser.Cookies;
            Credentials = parent != null ? parent.Credentials ?? window.Credentials : window.Credentials;
            EnableCLR = parent != null ? parent.EnableCLR : Browser.Options.EnableClr;
            EnableDebug = parent != null ? parent.EnableDebug : Browser.Options.EnableDebug;
            EnableJavascript = parent != null ? parent.EnableJavascript : Browser.Options.EnableJavascript;
            MasterContext = parent != null ? parent.MasterContext ?? parent : this;
            ParentContext = parent;
            Timeout = parent != null ? parent.Timeout : Browser.Options.Timeout;
            Uri = uri;
            Window = window;

            if (Browser.Options.EnableJavascript)
            {
                if (MasterContext == this)
                {
                    Script = new JintEngine();
                    Script.SetDebugMode(EnableDebug);
                    //Script.AllowClr = EnableCLR;
                }
                else
                    Script = MasterContext.Script;
            }
        }

        public RequestContext(BrowserWindow window, Uri uri)
            : this(null, window, uri)
        {
        }

        /// <summary>
        /// This is essentially clones the context.
        /// </summary>
        /// <param name="origin"></param>
        internal RequestContext(RequestContext origin)
        {
            Agent = origin.Agent;
            Browser = origin.Browser;
            Cookies = origin.Cookies;
            Credentials = origin.Credentials;
            EnableCLR = origin.EnableCLR;
            EnableDebug = origin.EnableDebug;
            EnableJavascript = origin.EnableJavascript;
            FromHistory = origin.FromHistory;
            LocalFilename = origin.LocalFilename;
            MasterContext = origin.MasterContext;
            ParentContext = origin.ParentContext;
            Script = origin.Script;
            Timeout = origin.Timeout;
            Uri = origin.Uri;
            Verb = origin.Verb;
            Window = origin.Window;
            PostVariables = new Dictionary<string, string>();
        }

        #endregion

        #region Properties

        /// <summary>
        /// The browser object in which this request originated.
        /// </summary>
        public WebBrowser Browser { get; set; }

        /// <summary>
        /// The user credentials (if any) to authenticate this request.
        /// </summary>
        public ICredentials Credentials { get; set; }

        /// <summary>
        /// The cookies container belonging to the browser.
        /// </summary>
        public CookieContainer Cookies { get; set; }

        /// <summary>
        /// True if the CLR can be used in scripting.
        /// </summary>
        public bool EnableCLR { get; set; }

        /// <summary>
        /// True if debug mode is enabled.
        /// </summary>
        public bool EnableDebug { get; set; }

        /// <summary>
        /// True if javascript should be executed.
        /// </summary>
        public bool EnableJavascript { get; set; }

        /// <summary>
        /// True if the request originated from the browser window history.
        /// </summary>
        public bool FromHistory { get; set; }

        public RequestContext GeneratedContext { get; set; }

        /// <summary>
        /// A filename if the response is to be written to disk.
        /// </summary>
        public string LocalFilename { get; set; }

        /// <summary>
        /// The topmost request context (e.g. top level document).
        /// </summary>
        public RequestContext MasterContext { get; set; }

        /// <summary>
        /// The parent context of this context.  It is possible to have a depth
        /// greater than 0 or 1.  The parent is one up and the MasterContext may
        /// be further up.
        /// </summary>
        public RequestContext ParentContext { get; set; }

        /// <summary>
        /// The actual data that was posted to the server.
        /// </summary>
        public string PostData { get; private set; }

        /// <summary>
        /// Variables to POST to server (if applicable).
        /// </summary>
        public Dictionary<string, string> PostVariables { get; set; }

        /// <summary>
        /// The refering Uri
        /// </summary>
        public Uri Referer { get; set; }

        /// <summary>
        /// The javascript engine used for evaluating script.
        /// </summary>
        public JintEngine Script { get; set; }

        /// <summary>
        /// The timeout period for this request (in miliseconds).
        /// </summary>
        public int Timeout { get; set; }

        /// <summary>
        /// The Uri of this request.
        /// </summary>
        public Uri Uri { get; protected set; }

        /// <summary>
        /// The HTTP Verb (if applicable) (e.g. GET, POST, etc).
        /// </summary>
        public string Verb { get; set; }

        /// <summary>
        /// The user agent to simulate when making requests.
        /// </summary>
        public UserAgent Agent { get; set; }

        /// <summary>
        /// The window into which this document is being loaded.
        /// </summary>
        public BrowserWindow Window { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Builds an appropriate request object based on the request scheme.
        /// </summary>
        /// <returns></returns>
        public WebRequest BuildRequest()
        {
            if (Uri.Scheme.ToLowerInvariant().StartsWith(HTMLConstants.HTML_SCHEME_FTP))
            {
                FtpWebRequest request = (FtpWebRequest) FtpWebRequest.Create(Uri);

                request.Timeout = Timeout;
                request.UseBinary = true;
                request.Credentials = Credentials;

                return request;
            }
            else if (Uri.Scheme.ToLowerInvariant().StartsWith(HTMLConstants.HTML_SCHEME_HTTP))
            {
                if (Verb.Equals(HTMLConstants.HTTP_VERB_GET))
                {
                    if (PostVariables.Count > 0)
                    {
                        StringBuilder sb = new StringBuilder(Uri.ToString());
                        char firstDelimiter = !String.IsNullOrEmpty(Uri.Query) ? '&' : '?';
                        int c = 0;

                        foreach (var p in PostVariables)
                        {
                            if (c++ == 0)
                                sb.Append(firstDelimiter);
                            else
                                sb.Append('&');
                            sb.Append(p.Key);
                            sb.Append('=');
                            sb.Append(p.Value);
                        }
                        this.Uri = new Uri(sb.ToString());
                    }
                }
                HttpWebRequest request = (HttpWebRequest) HttpWebRequest.Create(Uri);

                request.CookieContainer = Cookies;
                request.UserAgent = Agent.userAgent;
                request.KeepAlive = false;

                if (Timeout > 0)
                    request.Timeout = Timeout;

                if (!String.IsNullOrEmpty(Verb))
                    request.Method = Verb;

                request.Credentials = Credentials;

                if (Referer != null)
                    request.Referer = Referer.ToString();

                if (Verb.Equals(HTMLConstants.HTTP_VERB_POST) &&
                    String.IsNullOrEmpty(request.ContentType))
                {
                    StringBuilder sb = new StringBuilder();
                    int c = 0;

                    foreach (var p in PostVariables)
                    {
                        if (c++ > 0)
                            sb.Append('&');
                        sb.Append(p.Key);
                        sb.Append('=');
                        sb.Append(p.Value);
                    }
                    Byte[] data = Encoding.UTF8.GetBytes(sb.ToString());

                    request.ContentType = "application/x-www-form-urlencoded";
                    request.ContentLength = data.Length;

                    Stream dataStream = request.GetRequestStream();
                    dataStream.Write(data, 0, data.Length);
                    dataStream.Close();

                    PostData = sb.ToString();
                }
                return request;
            }
            else if (Uri.Scheme.ToLowerInvariant().StartsWith(HTMLConstants.HTML_SCHEME_FILE))
            {
                FileWebRequest request = (FileWebRequest)FileWebRequest.Create(Uri);

                request.Credentials = Credentials;

                return request;
            }
            else
            {
                throw new InvalidOperationException(
                    String.Format("'{0}' scheme is unsupported.", Uri.Scheme));
            }
        }

        /// <summary>
        /// Builds the initial script engine for the master context.
        /// </summary>
        /// <returns></returns>
        internal JintEngine BuildScriptContext(Document doc)
        {
            if (MasterContext == this)
            {
                WindowHelper jsWindow = new WindowHelper(this, doc);

                ((JsGlobal)Script.Global)["window"] = ((JsGlobal)Script.Global);

                Script
                    .SetParameter("_window", jsWindow)
                    .SetParameter("browser", Browser.Options.Agent)
                    .SetParameter("navigator", Browser.Options.Agent)
                    .SetParameter("document", doc)
                    .SetParameter("location", new Location(Uri))
                    .SetParameter("screen", new Screen())
                    .SetParameter("history", Window.History)
                    .SetParameter("navigator", Browser.Options.Agent);
                //  Load JSON namespace
                Script
                    .Run(JSON2.JsonText);
                Script
                    .Run(@"
window.Image = function()
{
    return document.createElement('img');
};
window.JSON = 
window.Option = function Option(t,v)
{
    var o = document.createElement('option');
    o.text = t;
    o.value = v;
    return o;
};
window.XMLHttpRequest = function() {
    return document.createXmlHttpRequest();
};
window.ActiveXObject = function(s) {
    return document.createActiveXObject(s);
};
window.addEventListener = function(eventName, listener, wantsCapture)
{
    document.addEventListener(eventName, listener, wantsCapture);
};
window.attachEvent = function(eventName, listener)
{
    document.attachEvent(eventName, listener);
};
document.location = location;
window.alert = function(x) { _window.alert(x); };
window.blur = function() { _window.blur(); };
window.browser = browser;
window.clearInterval = function (x) { _window.clearInterval(x); };
window.clearTimeout = function (x) { _window.clearTimeout(x); };
window.close = function() { _window.close(); };
window.closed = false;
window.confirm = function(msg) { _window.confirm(msg); };
window.createPopup = function() { };
window.defaultStatus = '';
window.document = document;
window.escape = function (s) { return _window.escape(s); };
window.focus = function(msg) { _window.focus(); };
window.frames = document.frames;
window.history = history;
window.localStorage = _window.localStorage;
window.location = location;
window.navigator = navigator;
window.screen = screen;
window.sessionStorage = _window.sessionStorage;
window.setInterval = function(code, timeout, st) { _window.setInterval(code, timeout); };
window.setTimeout = function(code, timeout, st) { _window.setTimeout(code, timeout); };
window.status = '';
window.prompt = function(msg, defaultText) { _window.prompt(msg, defaultText); };
window.unescape = function(s) { return _window.unescape(s); }");

                if (doc is HTMLDocument)
                {
                    HTMLDocument hdoc = doc as HTMLDocument;

                    hdoc.Register(Script);
                    Agent.RegisterWithScript(this, hdoc);
                }
                if (EnableDebug)
                {
                    Script.Step += (sender, info) =>
                    {
                        if (sender is ExecutionVisitor)
                        {
                            Console.WriteLine("{0} [Start: Line {2}, Char {1}, End: Line {4}, Char {3} = {4}",
                                info.CurrentStatement.Source.Code,
                                info.CurrentStatement.Source.Start.Char,
                                info.CurrentStatement.Source.Start.Line,
                                info.CurrentStatement.Source.Stop.Char,
                                info.CurrentStatement.Source.Stop.Line,
                                ((ExecutionVisitor)sender).Result);
                        }
                        else if (sender is JintEngine)
                        {
                            Console.WriteLine("{0} [Start: Line {2}, Char {1}, End: Line {4}, Char {3} = {4}",
                                info.CurrentStatement.Source.Code,
                                info.CurrentStatement.Source.Start.Char,
                                info.CurrentStatement.Source.Start.Line,
                                info.CurrentStatement.Source.Stop.Char,
                                info.CurrentStatement.Source.Stop.Line,
                                ((JintEngine)sender));
                        }
                        else
                        {
                            Console.WriteLine("{0} [Start: Line {2}, Char {1}, End: Line {4}, Char {3} = {4}",
                                info.CurrentStatement.Source.Code,
                                info.CurrentStatement.Source.Start.Char,
                                info.CurrentStatement.Source.Start.Line,
                                info.CurrentStatement.Source.Stop.Char,
                                info.CurrentStatement.Source.Stop.Line,
                                sender);
                        }
                    };
                    Script.SetDebugMode(true);
                }
            }
            return Script;
        }

        /// <summary>
        /// Executes any on-type event handlers.
        /// </summary>
        /// <param name="p"></param>
        /// <param name="target"></param>
        /// <param name="eventName"></param>
        internal void ExecuteEvent(IEvent p, object target, string eventName)
        {
            PropertyInfo pi = target.GetType().GetProperty(eventName.ToLowerInvariant());

            if (Script == null)
                return;

            if (pi != null)
            {
                try
                {
                    var o = pi.GetValue(target, null);

                    if (o != null)
                    {
                        //  Event initialization
                        Script.SetParameter("event", p);
                        Script.Run("window['event'] = event;");

                        if (o is string)
                        {
                            var s = o as string;

                            //  Strip off legacy stuff
                            if (s.ToLowerInvariant().StartsWith("javascript:"))
                                s = s.Substring(11).Trim();

                            try
                            {
                                if (!s.Contains("(") && !s.Contains("=") && !s.Contains(";"))
                                {
                                    Script.CallFunction(s, p);
                                }
                                else
                                {
                                    //  This is a total hack to around the use of the word 'this' in the
                                    //  context of an "on" handler.
                                    s = s.Replace("this.", "_____this.");
                                    Script.SetParameter("_____this", target);
                                    var result = Script.Run(s);
                                    Script.Run("_____this = undefined;");
                                }
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("ExecuteEvent() failed with '{0}'", ex.Message);
                            }
                        }
                    }
                    else if (o is JsFunction)
                    {
                        Script.CallFunction((o as JsFunction), p);
                    }

                    //  Event cleanup
                    Script.Run("delete window['event']; event = null;");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }

        /// <summary>
        /// Helper method to resolve requests relative to this one to full-qualified
        /// Uris.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public Uri ResolveUri(string path)
        {
            return new Uri(this.Uri, new Uri(path, UriKind.RelativeOrAbsolute));
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (Script != null && ParentContext == null)
            {
            }
        }

        #endregion

        internal RequestContext ResolveNewContext(string p)
        {
            Uri resolved = ResolveUri(p);

            RequestContext newContext = new RequestContext()
            {
                Agent = Agent,
                Browser = Browser,
                Cookies = Cookies,
                Credentials = Credentials,
                EnableCLR = EnableCLR,
                EnableDebug = EnableDebug,
                EnableJavascript = EnableJavascript,
                FromHistory = FromHistory,
                LocalFilename = LocalFilename,
                MasterContext = null,
                ParentContext = null,
                Timeout = Browser.Options.Timeout,
                Uri = resolved,
                Verb = "GET",
                Window = Window
            };

            if (newContext.EnableJavascript)
            {
                newContext.Script = new JintEngine();
                newContext.Script.SetDebugMode(newContext.EnableDebug);
                newContext.MasterContext = newContext;
            }

            return newContext;
        }
    }
}
